﻿using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using LogikBug.Async.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace LogikBug.Async.Tasks
{
	/// <summary>
	/// The base <see cref="AgentController"/> class for all derived controllers.
	/// </summary>
	[Guid("5cbecb0f-7798-498a-9c37-d10109a5a2c1")]
	public class AgentController : DisposableObject
	{
		#region Events

		#region Static Events

		/// <summary>
		/// The global event that gets invoked when an <see cref="Agent"/> in
		/// the current <see cref="AppDomain"/> are beginning their execution.
		/// </summary>
		public static event Action<AgentController, AgentContext> OnAgentStartedGlobal;

		/// <summary>
		/// The global event that gets invoked when an <see cref="Agent"/> in
		/// the current <see cref="AppDomain"/> that are disposing their resouces.
		/// </summary>
		public static event Action<AgentController, AgentContext, AgentResult> OnAgentDisposingGlobal;

		/// <summary>
		/// The global event that gets invoked when an <see cref="Agent"/> in
		/// the current <see cref="AppDomain"/> that have completed their execution.
		/// </summary>
		public static event Action<AgentController, AgentContext, AgentResult> OnAgentCompletedGlobal;

		/// <summary>
		/// The global event that gets invoked when an <see cref="Agent"/> in the current
		/// <see cref="AppDomain"/> has thrown an <see cref="Exception"/> while executing.
		/// </summary>
		public static event Action<AgentController, AgentContext, AgentResult, Exception> OnAgentErroredGlobal;

		#endregion

		#region Instance Events

		/// <summary>
		/// The event that is invoked when the current instance is disposing.
		/// </summary>
		public event Action ControllerDispose;

		/// <summary>
		/// The event that is invoked when an <see cref="Agent"/> that's been
		/// added to this particular instance has starting it's execution.
		/// </summary>
		public event Action<AgentContext> OnAgentStarted;

		/// <summary>
		/// The event that is invoked when an <see cref="Agent"/> that's been
		/// added to this particular instance is disposing of it's resources.
		/// </summary>
		public event Action<AgentContext, AgentResult> OnAgentDisposing;

		/// <summary>
		/// The event that is invoked when an <see cref="Agent"/> that's been
		/// added to this particular instance has completed it's execution.
		/// </summary>
		public event Action<AgentContext, AgentResult> OnAgentCompleted;

		/// <summary>
		/// The event that is invoked when an <see cref="Agent"/> that's been
		/// added to this particular instance has thrown an <see cref="Exception"/>.
		/// </summary>
		public event Action<AgentContext, AgentResult, Exception> OnAgentErrored;

		#endregion

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="AgentController"/>
		/// using the default <see cref="AgentSettings"/>.
		/// </summary>
		public AgentController() : this(new AgentSettings())
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="AgentController"/>
		/// using the specified <paramref name="controllerEventHandler"/>.
		/// </summary>
		/// <param name="controllerEventHandler">The object that handles the events of this <see cref="AgentController"/>.</param>
		public AgentController(IAgentControllerEventHandler controllerEventHandler) : this(new AgentSettings(), controllerEventHandler)
		{
		}

		/// <summary>
		/// Initializes a new instance of the
		/// <see cref="AgentController"/> using the specified <paramref name="settings"/>.
		/// </summary>
		/// <param name="settings">The <see cref="AgentSettings"/>
		/// used in the initialization of the current instance.</param>
		public AgentController(AgentSettings settings)
		{
			Initialize(settings);
		}

		/// <summary>
		/// Initializes a new instance of the
		/// <see cref="AgentController"/> using the specified <paramref name="settings"/>.
		/// </summary>
		/// <param name="settings">The <see cref="AgentSettings"/>
		/// used in the initialization of the current instance.</param>
		/// <param name="controllerEventHandler">The object that handles the events of this <see cref="AgentController"/>.</param>
		public AgentController(AgentSettings settings, IAgentControllerEventHandler controllerEventHandler)
		{
			ControllerEventHandler = controllerEventHandler;
			Initialize(settings);
		}

		#endregion

		#region Properties

		/// <summary>
		/// Gets the <see cref="ConcurrentQueue{T}"/> for this instance.
		/// </summary>
		protected ConcurrentQueue<Agent> AgentQueue { get; set; }

		/// <summary>
		/// Gets or sets the current <see cref="ConcurrentList{T}"/>
		/// that holds all currently executing <see cref="AgentContext"/>s.
		/// </summary>
		protected ConcurrentList<AgentContext> AgentContexts { get; set; }

		/// <summary>
		/// Gets the <see cref="IAgentControllerEventHandler"/> for this instance.
		/// </summary>
		protected IAgentControllerEventHandler ControllerEventHandler { get; set; }

		/// <summary>
		/// Gets the total number of <see cref="Agent"/>s in queue.
		/// </summary>
		public virtual int AgentQueueCount
		{
			get
			{
				return AgentQueue.Count;
			}
		}

		/// <summary>
		/// Gets the total number of <see cref="AgentContext"/>s currently executing.
		/// </summary>
		public virtual int AgentContextCount
		{
			get
			{
				return AgentContexts.Count;
			}
		}

		/// <summary>
		/// Gets the <see cref="AgentSettings"/> for the current instance.
		/// </summary>
		public AgentSettings Settings { get; private set; }

		/// <summary>
		/// Gets the unique identifier for the current <see cref="AgentController"/>.
		/// <remarks>
		/// This value is set using the <see cref="System.Runtime.InteropServices.GuidAttribute"/>.
		/// </remarks>
		/// </summary>
		public Guid TypeId
		{
			get { return GetType().GUID; }
		}

		#endregion

		#region Methods

		#region Private Methods

		private void AgentCompleted(AgentContext context, AgentResult result)
		{
			InvokeAgentComplete(context, result);
		}

		private void AgentDisposing(AgentContext context, AgentResult result)
		{
			InvokeAgentDisposing(context, result);
		}

		private void AgentErrored(AgentContext context, AgentResult result, Exception exception)
		{
			InvokeAgentErrored(context, result, exception);
		}

		private void AgentStarted(AgentContext context)
		{
			InvokeAgentStarted(context);
		}

		#endregion

		#region Protected Methods

		/// <summary>
		/// Adds an <see cref="AgentContext"/> to the <see cref="AgentContexts"/>.
		/// (see property: <seealso cref="AgentContexts"/>).
		/// </summary>
		/// <param name="context">The <see cref="AgentContext"/> to add.</param>
		protected virtual void AddAgentContext(AgentContext context)
		{
			AgentContexts.TryAdd(context);
		}

		/// <summary>
		/// Disposes of the current <paramref name="context"/>
		/// and activates the next one in the queue.
		/// </summary>
		/// <param name="context">The <see cref="AgentContext"/> to dispose of.</param>
		protected virtual void DisposeContextAndActivateNext(AgentContext context)
		{
			// Dispose of the current AgentContext.
			DisposeAgentContext(context);

			// Make sure we have Agents queued and that the number of concurrently
			// executing AgentContexts is less then the maximum number we are allowed.
			// If so, we try to dequeue an Agent from the queue and initialize it.
			if (AgentQueueCount > 0 && AgentContextCount < Settings.MaxActiveAgents)
			{
				Agent agent;

				if (AgentQueue.TryDequeue(out agent))
				{
					InitializeAgent(agent);
				}
			}
		}

		/// <summary>
		/// Adds the <paramref name="agent"/> to the <see cref="AgentQueue"/>.
		/// </summary>
		/// <param name="agent">The <see cref="Agent"/> to Enqueue.</param>
		protected virtual void Enqueue(Agent agent)
		{
			AgentQueue.Enqueue(agent);
		}

		/// <summary>
		/// Creates a new <see cref="AgentContext"/> for the <paramref name="agent"/>
		/// and calls the Start method of the <see cref="AgentContext"/> it created. 
		/// </summary>
		/// <remarks>
		/// This method is for internal purposes and should only be called from InitializeAgent.
		/// </remarks>
		/// <param name="agent">The <see cref="Agent"/> to execute.</param>
		protected virtual void Execute(Agent agent)
		{
			// If we aren't disposed or currently disposing
			// then create a new AgentContext and call it's
			// start method to start the thread.
			if (AssertNotDisposedOrDisposing())
			{
				var context = new AgentContext(Settings, agent, this);
				ControllerDispose += context.ControllerDispose;
				AddAgentContext(context);
				context.Start();
			}
		}

		/// <summary>
		/// Removes the <see cref="AgentContext"/> from the <see cref="AgentContexts"/>
		/// (see property: <seealso cref="AgentContexts"/>).
		/// </summary>
		/// <param name="context">The <see cref="AgentContext"/> to remove.</param>
		protected virtual void RemoveAgentContext(AgentContext context)
		{
			AgentContexts.TryRemove(context);
		}

		/// <summary>
		/// Invokes the instance and static AgentCompleted events for the
		/// current <paramref name="context"/> and <paramref name="result"/>.
		/// </summary>
		/// <param name="context">The current <see cref="AgentContext"/> that has completed.</param>
		/// <param name="result">The <see cref="AgentResult"/> of the executed <see cref="Agent"/>.</param>
		protected virtual void InvokeAgentComplete(AgentContext context, AgentResult result)
		{
			// Remove the current AgentContext.
			RemoveAgentContext(context);

			// Invoke the instance's event.
			if (OnAgentCompleted != null)
			{
				OnAgentCompleted(context, result);
			}

			// Invoke the global event.
			if (OnAgentCompletedGlobal != null)
			{
				OnAgentCompletedGlobal(this, context, result);
			}

			// Dispose of the AgentContext and
			// activate the next Agent in queue.
			DisposeContextAndActivateNext(context);
		}

		/// <summary>
		/// Invokes the instance and static AgentDisposing events for the
		/// current <paramref name="context"/> and <paramref name="result"/>.
		/// </summary>
		/// <param name="context">The current <see cref="AgentContext"/> that is disposing.</param>
		/// <param name="result">The <see cref="AgentResult"/> of the executed <see cref="Agent"/>.</param>
		protected virtual void InvokeAgentDisposing(AgentContext context, AgentResult result)
		{
			// Invoke the instance's event
			if (OnAgentDisposing != null)
			{
				OnAgentDisposing(context, result);
			}

			// Invoke the global event.
			if (OnAgentDisposingGlobal != null)
			{
				OnAgentDisposingGlobal(this, context, result);
			}
		}

		/// <summary>
		/// Invokes the instance and static AgentErrored events for the
		/// current <paramref name="context"/>, <paramref name="result"/>, and <see cref="Exception"/>.
		/// </summary>
		/// <param name="context">The current <see cref="AgentContext"/> that has thrown the <see cref="Exception"/>.</param>
		/// <param name="result">The <see cref="AgentResult"/> of the executed <see cref="Agent"/>.</param>
		/// <param name="e">The exception that occurred while executing the <see cref="Agent"/>.</param>
		protected virtual void InvokeAgentErrored(AgentContext context, AgentResult result, Exception e)
		{
			// Invoke the instance's event
			if (OnAgentErrored != null)
			{
				OnAgentErrored(context, result, e);
			}

			// Invoke the global event.
			if (OnAgentErroredGlobal != null)
			{
				OnAgentErroredGlobal(this, context, result, e);
			}
		}

		/// <summary>
		/// Invokes the instance and static AgentStarted events for the current <paramref name="context"/>.
		/// </summary>
		/// <param name="context">The current <see cref="AgentContext"/> being started.</param>
		protected virtual void InvokeAgentStarted(AgentContext context)
		{
			// Invoke the instance's event
			if (OnAgentStarted != null)
			{
				OnAgentStarted(context);
			}

			// Invoke the global event.
			if (OnAgentStartedGlobal != null)
			{
				OnAgentStartedGlobal(this, context);
			}
		}

		/// <summary>
		/// Disposes the resources associated with this current instance.
		/// </summary>
		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				int count = AgentQueue.Count;

				if (count > 0)
				{
					Agent agent;

					for (int i = 0; i < count; i++)
					{
						if (AgentQueue.TryDequeue(out agent))
						{
							agent.Dispose();
						}
					}
				}

				// If we have agents currently executing, go ahead and let
				// them know that our controller is disposing by calling the event.
				if (AgentContextCount > 0 && ControllerDispose != null)
				{
					ControllerDispose();
				}
			}
		}

		/// <summary>
		/// Initializes the current instance using the specified <paramref name="settings"/>.
		/// </summary>
		/// <param name="settings"></param>
		protected void Initialize(AgentSettings settings)
		{
			// New stuff up, call the initialize method and Subscribe to events.
			Settings = settings ?? new AgentSettings();
			AgentQueue = new ConcurrentQueue<Agent>();
			AgentContexts = new ConcurrentList<AgentContext>();

			Initialize();

			OnAgentCompleted += AgentControllerOnAgentCompleted;
			OnAgentDisposing += AgentControllerOnAgentDisposing;
			OnAgentErrored += AgentControllerOnAgentErrored;
			OnAgentStarted += AgentControllerOnAgentStarted;
		}

		/// <summary>
		/// Initializes and executes the current <see cref="Agent"/>.
		/// </summary>s
		/// <param name="agent">The <see cref="Agent"/> to initialize and execute.</param>
		protected void InitializeAgent(Agent agent)
		{
			if (agent != null)
			{
				// Subscribe to all the agent's events and execute it.
				agent.OnStarted += AgentStarted;
				agent.OnCompleted += AgentCompleted;
				agent.OnDisposing += AgentDisposing;
				agent.OnErrored += AgentErrored;

				Execute(agent);
			}
		}

		/// <summary>
		/// Disposes the current <see cref="AgentContext"/>.
		/// </summary>
		/// <param name="context">The <see cref="AgentContext"/> to dispose of.</param>
		protected void DisposeAgentContext(AgentContext context)
		{
			// Unsubscribe the AgentContext's dispose
			// listener and call it's dispose method.
			ControllerDispose -= context.ControllerDispose;
			context.Dispose();
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Checks to see if the current <see cref="AgentContextCount"/> is
		/// less than the maximun number allowed for the current instance
		/// and calls InitializeAgent to initialize and activate the
		/// <paramref name="agent"/>, otherwise calls Enqueue to queue the <paramref name="agent"/>s execution.
		/// </summary>
		/// <param name="agent">The <see cref="Agent"/> to activate.</param>
		public void Activate(Agent agent)
		{
			// If our number of concurrently executing AgentContexts
			// is less then the max we are allowed, then initialize it,
			// otherwise we will queue it up.
			if (AgentContextCount < Settings.MaxActiveAgents)
			{
				InitializeAgent(agent);
			}
			else
			{
				Enqueue(agent);
			}
		}

		/// <summary>
		/// Adds an <see cref="Agent"/> to the queue if the total number
		/// of concurrent executing <see cref="Agent"/>s have been reached, 
		/// otherwise will call activate <paramref name="agent"/> to execute the agent.
		/// </summary>
		/// <remarks>
		/// This is the main access point to add an <see cref="Agent"/>.
		/// All other methods of adding an <see cref="Agent"/> are meant for internal use and
		/// should be avoided unless directly deriving from this class.
		/// </remarks>
		/// <param name="agent">The <see cref="Agent"/> to add to the queue or execute.</param>
		public virtual void Add(Agent agent)
		{
			Enqueue(agent);
		}

		/// <summary>
		/// Adds an <see cref="Agent"/> to the queue if the total number
		/// of concurrent executing <see cref="Agent"/>s have been reached, 
		/// otherwise will call activate <paramref name="agent"/> to execute the agent.
		/// </summary>
		/// <remarks>
		/// This is the main access point to add an <see cref="Agent"/>.
		/// All other methods of adding an <see cref="Agent"/> are meant for internal use and
		/// should be avoided unless directly deriving from this class.
		/// </remarks>
		/// <param name="agent">The <see cref="Agent"/> to add to the queue or execute.</param>
		[Obsolete("This method is deprecated and will be removed in a future release. Use Add(Agent) instead.")]
		public virtual void AddAgent(Agent agent)
		{
			Enqueue(agent);
		}

		/// <summary>
		/// Called when the current <see cref="AgentController"/> is initializing. No default implementation.
		/// </summary>
		public virtual void Initialize()
		{
		}

		/// <summary>
		/// Starts the execution of all the <see cref="Agent"/>s in the queue.
		/// </summary>
		public virtual void Run()
		{
			if (AgentQueueCount > 0)
			{
				Task.Factory.StartNew(() =>
				{
					int count = Settings.MaxActiveAgents;

					for (int i = 0; i < count; i++)
					{
						Agent agent;

						if (!AgentQueue.TryDequeue(out agent))
						{
							break;
						}

						Activate(agent);
					}
				});
			}
		}

		/// <summary>
		/// The method that is called when a new <see cref="Agent"/> is started.
		/// </summary>
		public virtual void AgentControllerOnAgentStarted(AgentContext context)
		{
			if (ControllerEventHandler != null)
			{
				ControllerEventHandler.AgentStarted(this, context);
			}
		}

		/// <summary>
		/// The method that is called when an <see cref="Agent"/> is disposing of it's resources.
		/// </summary>
		public virtual void AgentControllerOnAgentDisposing(AgentContext context, AgentResult result)
		{
			if (ControllerEventHandler != null)
			{
				ControllerEventHandler.AgentDisposing(this, context, result);
			}
		}

		/// <summary>
		/// The method that is called when an <see cref="Agent"/> is finished executing.
		/// </summary>
		public virtual void AgentControllerOnAgentCompleted(AgentContext context, AgentResult result)
		{
			if (ControllerEventHandler != null)
			{
				ControllerEventHandler.AgentCompleted(this, context, result);
			}
		}

		/// <summary>
		/// The method that is called when an <see cref="Agent"/> has thrown an <see cref="Exception"/>.
		/// </summary>
		public virtual void AgentControllerOnAgentErrored(AgentContext context, AgentResult result, Exception exception)
		{
			if (ControllerEventHandler != null)
			{
				ControllerEventHandler.AgentErrored(this, context, result, exception);
			}
		}

		#endregion

		#endregion
	}
}